using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using log4net;
using CodeMadeSimple.PropertiesManagement;
using CodeMadeSimple.Common.Validation;

namespace EnvRide.Core.ObjectModel
{
    /// <summary>
    /// This class represents an EnvRide environment. Environment are the containers of overrides.
    /// </summary>
    public class Environment : ObjectWithIncludes, IXmlSerializable
    {
        #region Fields

        private static ILog _logger = log4net.LogManager.GetLogger(typeof(Environment));
        private AttributeOverrideCollection _attributeOverrides;
        private ElementOverrideCollection _elementOverrides;
        private Environment _baseEnvironment;

        #endregion

        #region Properties

        /// <summary>
        /// List of attribute overrides.
        /// </summary>
        public AttributeOverrideCollection AttributeOverrides
        {
            get { return _attributeOverrides; }
            set { _attributeOverrides = value; }
        }

        /// <summary>
        /// Base environment name.
        /// </summary>
        public string BaseEnvironmentName { get; set; }

        /// <summary>
        /// Base environment project file path.
        /// </summary>
        public string BaseProjectFilePath { get; set; }

        /// <summary>
        /// List of element overrides.
        /// </summary>
        public ElementOverrideCollection ElementOverrides
        {
            get { return _elementOverrides; }
            set { _elementOverrides = value; }
        }

        /// <summary>
        /// Environment's name.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Output file's path.
        /// </summary>
        public string OutputFilePath { get; set; }

        [XmlIgnore]
        /// <summary>
        /// Environment owner project.
        /// </summary>
        public EnvRideProject Project { get; set; }

        #endregion

        #region Methods

        /// <summary>
        /// Process the environment.
        /// </summary>
        /// <param name="args">Processing arguments.</param>
        public void Process(EnvRideProcessArguments args)
        {
            if (args == null)
                throw new ArgumentNullException("args");

            using (ThreadContext.Stacks[EnvRideProject.ENV_RIDE_LOG_STACK].Push(this.ToString()))
            {
                ProcessInternal(args);

                String fullOutputFilePath = OutputFilePath;

                if (!String.IsNullOrEmpty(args.OutputFilePath))
                {
                    // If the output file was supplied, overrides the one from the environment
                    fullOutputFilePath = args.OutputFilePath;
                }

                if (!String.IsNullOrEmpty(fullOutputFilePath))
                {
                    fullOutputFilePath = args.ParseValue(fullOutputFilePath);

                    if (!Path.IsPathRooted(fullOutputFilePath))
                        fullOutputFilePath = Path.Combine(Project.BaseDirectory, fullOutputFilePath);

                    FileInfo info = new FileInfo(fullOutputFilePath);

                    if (!info.Exists || !args.SaveOutputFileOnlyIfItHasChanges || HasChanges(info, args.Document))
                    {
                        if (_logger.IsInfoEnabled)
                            _logger.InfoFormat("Saving to: {0}", fullOutputFilePath);

                        String parentDirectory = Path.GetDirectoryName(fullOutputFilePath);

                        if (args.CreateOutputFileDirectory && !Directory.Exists(parentDirectory))
                        {
                            Directory.CreateDirectory(parentDirectory);
                        }

                        // Save only if it should not check for changes or if the file has changes.
                        args.Document.Save(fullOutputFilePath);
                    }
                    else
                    {
                        if (_logger.IsWarnEnabled)
                            _logger.WarnFormat("Since there are no changes, output file \"{0}\" was not written.", fullOutputFilePath);
                    }
                }
            }
        }

        private bool HasChanges(FileInfo existingOutputFile, XmlDocument xmlDocument)
        {
            bool hasChanges = false;

            using (MemoryStream ms = new MemoryStream())
            {
                xmlDocument.Save(ms);

                hasChanges = existingOutputFile.Length != ms.Length;

                if (!hasChanges)
                {
                    ms.Position = 0;

                    using (BinaryReader fileReader = new BinaryReader(existingOutputFile.OpenRead()))
                    using (BinaryReader msReader = new BinaryReader(ms))
                    {
                        long i = 0;
                        long length = existingOutputFile.Length;

                        do
                        {
                            hasChanges = fileReader.ReadByte() != msReader.ReadByte();

                            i++;
                        }
                        while (!hasChanges && i < length);
                    }
                }
            }

            return hasChanges;
        }

        private void ProcessInternal(EnvRideProcessArguments args)
        {
            if (_logger.IsInfoEnabled)
                _logger.Info("Processing starting...");

            ProcessPropertiesReferences(args);

            ProcessBaseEnvironment(args);

            ProcessIncludedAttributeOverrides(args);

            ProcessIncludedElementOverrides(args);

            ProcessAttributeOverrides(args);

            ProcessElementOverrides(args);

            if (_logger.IsInfoEnabled)
                _logger.Info("Processing finished.");
        }

        private void ProcessPropertiesReferences(EnvRideProcessArguments args)
        {
            if (args.PropertyContainer != null)
            {
                args.Document.LoadXml(args.PropertyContainer.ParseValue(args.Document.OuterXml, args.PropertyNotFoundAction));
            }
        }

        public void ProcessElementOverrides(EnvRideProcessArguments args)
        {
            if (_elementOverrides != null && _elementOverrides.Count > 0)
            {
                if (_logger.IsDebugEnabled)
                    _logger.Debug("Processing element overrides");

                foreach (ElementOverride elemOver in _elementOverrides)
                {
                    elemOver.Process(args);
                }
            }
        }

        public void ProcessAttributeOverrides(EnvRideProcessArguments args)
        {
            if (_attributeOverrides != null && _attributeOverrides.Count > 0)
            {
                if (_logger.IsDebugEnabled)
                    _logger.Debug("Processing attribute overrides");

                foreach (AttributeOverride attOver in _attributeOverrides)
                {
                    attOver.Process(args);
                }
            }
        }

        private void ProcessIncludedElementOverrides(EnvRideProcessArguments args)
        {
            if (Project.IncludedElementOverrides != null && Project.IncludedElementOverrides.Count > 0)
            {
                if (_logger.IsDebugEnabled)
                    _logger.Debug("Processing included element overrides from project");

                foreach (ElementOverrideInclude elemOverInc in Project.IncludedElementOverrides)
                {
                    elemOverInc.Process(args);
                }
            }

            if (IncludedElementOverrides != null && IncludedElementOverrides.Count > 0)
            {
                if (_logger.IsDebugEnabled)
                    _logger.Debug("Processing included element overrides");

                foreach (ElementOverrideInclude elemOverInc in IncludedElementOverrides)
                {
                    elemOverInc.Process(args);
                }
            }
        }

        private void ProcessIncludedAttributeOverrides(EnvRideProcessArguments args)
        {
            if (Project.IncludedAttributeOverrides != null && Project.IncludedAttributeOverrides.Count > 0)
            {
                if (_logger.IsDebugEnabled)
                    _logger.DebugFormat("Processing included attribute overrides from project", BaseEnvironmentName);

                foreach (AttributeOverrideInclude attOverInc in Project.IncludedAttributeOverrides)
                {
                    attOverInc.Process(args);
                }
            }

            if (IncludedAttributeOverrides != null && IncludedAttributeOverrides.Count > 0)
            {
                if (_logger.IsDebugEnabled)
                    _logger.DebugFormat("Processing included attribute overrides", BaseEnvironmentName);

                foreach (AttributeOverrideInclude attOverInc in IncludedAttributeOverrides)
                {
                    attOverInc.Process(args);
                }
            }
        }

        private void ProcessBaseEnvironment(EnvRideProcessArguments args)
        {
            Environment baseEnv = GetBaseEnvironment(args);

            if (baseEnv != null)
            {
                using (ThreadContext.Stacks[EnvRideProject.ENV_RIDE_LOG_STACK].Push(baseEnv.ToString()))
                {
                    baseEnv.ProcessInternal(args);
                }
            }
        }

        public Environment GetBaseEnvironment(EnvRideProcessArguments args)
        {
            Environment baseEnv = null;

            if (!String.IsNullOrEmpty(BaseEnvironmentName))
            {
                if (_baseEnvironment == null)
                {
                    EnvRideProject baseProject = null;

                    if (!String.IsNullOrEmpty(BaseProjectFilePath))
                    {
                        String baseProjectFilePath = args.ParseValue(BaseProjectFilePath);

                        if (_logger.IsDebugEnabled)
                            _logger.DebugFormat("Getting base environment: {0} - {1}", baseProjectFilePath, BaseEnvironmentName);

                        baseProject = EnvRideProject.Deserialize(Project.BaseDirectory, baseProjectFilePath);
                    }
                    else
                    {
                        if (_logger.IsDebugEnabled)
                            _logger.DebugFormat("Getting base environment: {0}", BaseEnvironmentName);

                        baseProject = Project;
                    }

                    baseEnv = baseProject.Environments[BaseEnvironmentName];

                    if (baseEnv == null)
                    {
                        args.HasErrors = true;
                        _logger.ErrorFormat("The base environment \"{0}\" was not found at project \"{1}\"!", BaseEnvironmentName, baseProject);
                    }
                }
                else
                    baseEnv = _baseEnvironment;
            }

            return baseEnv;
        }

        #endregion

        #region IXmlSerializable Members

        public System.Xml.Schema.XmlSchema GetSchema()
        {
            throw new NotImplementedException();
        }

        public void ReadXml(XmlReader reader)
        {
            BaseEnvironmentName = reader.GetAttribute("baseEnvironmentName");
            Name = reader.GetAttribute("name");
            OutputFilePath = reader.GetAttribute("outputFilePath");
            BaseProjectFilePath = reader.GetAttribute("baseProjectFilePath");

            if (!reader.IsEmptyElement)
            {
                while (reader.Read() && reader.Name != "Environment")
                {
                    if (reader.Name.Equals("AttributeOverrides") && !reader.IsEmptyElement)
                    {
                        _attributeOverrides = new AttributeOverrideCollection();

                        while (reader.Read() && reader.Name != "AttributeOverrides")
                        {
                            if (reader.Name.Equals("AttributeOverride"))
                            {
                                AttributeOverride attOver = new AttributeOverride();

                                attOver.ReadXml(reader);

                                _attributeOverrides.Add(attOver);
                            }
                        }
                    }
                    else if (reader.Name.Equals("ElementOverrides") && !reader.IsEmptyElement)
                    {
                        _elementOverrides = new ElementOverrideCollection();

                        while (reader.Read() && reader.Name != "ElementOverrides")
                        {
                            while (reader.Name.Equals("ElementOverride"))
                            {
                                ElementOverride elemOver = new ElementOverride();

                                elemOver.ReadXml(reader);

                                _elementOverrides.Add(elemOver);
                            }

                            if (reader.Name == "ElementOverrides")
                                break;
                        }
                    }
                    else if (reader.Name.Equals("IncludedAttributeOverrides"))
                    {
                        ReadIncludedAttributesOverrides(reader);
                    }
                    else if (reader.Name.Equals("IncludedElementOverrides"))
                    {
                        ReadIncludedElementOverrides(reader);
                    }
                }
            }
        }

        public void WriteXml(XmlWriter writer)
        {
            if (!String.IsNullOrEmpty(BaseEnvironmentName))
                writer.WriteAttributeString("baseEnvironmentName", BaseEnvironmentName);

            if (!String.IsNullOrEmpty(Name))
                writer.WriteAttributeString("name", Name);

            if (!String.IsNullOrEmpty(OutputFilePath))
                writer.WriteAttributeString("outputFilePath", OutputFilePath);

            if (!String.IsNullOrEmpty(BaseProjectFilePath))
                writer.WriteAttributeString("baseProjectFilePath", BaseProjectFilePath);

            if (_attributeOverrides != null)
            {
                writer.WriteStartElement("AttributeOverrides");

                foreach (AttributeOverride attOver in _attributeOverrides)
                {
                    writer.WriteStartElement("AttributeOverride");

                    attOver.WriteXml(writer);

                    writer.WriteEndElement();
                }

                writer.WriteEndElement();
            }

            if (_elementOverrides != null)
            {
                writer.WriteStartElement("ElementOverrides");

                foreach (ElementOverride attOver in _elementOverrides)
                {
                    writer.WriteStartElement("ElementOverride");

                    attOver.WriteXml(writer);

                    writer.WriteEndElement();
                }

                writer.WriteEndElement();
            }

            WriteIncludesXml(writer);
        }

        #endregion

        public override string ToString()
        {
            if (!String.IsNullOrEmpty(Name))
                return Name;
            else
                return "Environment";
        }

        #region IValidatable Members

        public override string GetValidationId()
        {
            return ToString();
        }

        protected override void AddValidationMessages(ValidationResult result)
        {
            base.AddValidationMessages(result);

            if (String.IsNullOrEmpty(Name))
            {
                result.AddMessage(new RequiredValueValidationMessage("Name"));
            }

            if (_attributeOverrides != null && _attributeOverrides.Count > 0)
            {
                ValidationResult attOversValResult = new ValidationResult("AttributeOverrides");

                foreach (AttributeOverride attOver in _attributeOverrides)
                {
                    attOversValResult.AddChildResult(attOver.Validate());
                }

                result.AddChildResult(attOversValResult);
            }

            if (_elementOverrides != null && _elementOverrides.Count > 0)
            {
                ValidationResult elemOversValResult = new ValidationResult("ElementOverrides");

                foreach (ElementOverride elemOver in _elementOverrides)
                {
                    elemOversValResult.AddChildResult(elemOver.Validate());
                }

                result.AddChildResult(elemOversValResult);
            }
        }

        #endregion
    }
}